#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Continuous integration toolkit. The purpose of this script is to generate
everything which is needed for the CI environment.

List of responsibilities:
    * execute default suites
    * terminate execution if any of internal scenarios fails
    * creates the report file that is packaged and exposed under Github
      (.quality_assurance_status).
"""

import argparse
import atexit
import copy
import json
import os
import subprocess
import sys
import tempfile
import time

import yaml

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
TESTS_DIR = os.path.join(SCRIPT_DIR, "tests")
BASE_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", ".."))
BUILD_DIR = os.path.join(BASE_DIR, "build")
LOG_LEVEL = "TRACE"


def check_server_process(process):
    # Check if the process is still running
    print("Memgraph PID: ", process.pid, flush=True)
    exit_code = process.poll()
    if exit_code is None:
        print("Process is still running but server is not responding", flush=True)

        # Get basic process information using standard library
        try:
            # Try to send signal 0 to check if process exists
            os.kill(process.pid, 0)
            print("Process exists and is accessible", flush=True)

            # Try to get process info from /proc (Linux only)
            try:
                with open(f"/proc/{process.pid}/stat", "r") as f:
                    stat_info = f.read().split()
                    if len(stat_info) > 2:
                        state = stat_info[2]
                        print(f"Process state from /proc: {state}", flush=True)
                        if state == "Z":
                            print("WARNING: Process is in zombie state!", flush=True)
                        elif state == "T":
                            print("Process is stopped", flush=True)
                        elif state == "S":
                            print("Process is sleeping", flush=True)
                            try:
                                with open(f"/proc/{process.pid}/wchan", "r") as f:
                                    wchan = f.read().strip()
                                    if wchan:
                                        print(f"Process wait channel: {wchan}", flush=True)
                            except (FileNotFoundError, PermissionError):
                                print("Cannot access /proc wchan info", flush=True)
                        elif state == "R":
                            print("Process is running", flush=True)
            except (FileNotFoundError, PermissionError):
                print("Cannot access /proc info (not Linux or no permission)", flush=True)

        except ProcessLookupError:
            print("Process no longer exists", flush=True)
        except PermissionError:
            print("No permission to check process", flush=True)
        except Exception as e:
            print(f"Could not get process details: {e}", flush=True)

    else:
        print(f"Process has exited with code: {exit_code}", flush=True)

    return exit_code


def wait_for_server(port, process, delay=0.01):
    cmd = ["nc", "-z", "-w", "1", "127.0.0.1", str(port)]
    count = 0
    while subprocess.call(cmd) != 0:
        time.sleep(0.01)
        if count > 20 / 0.01:
            print("Could not wait for server on port", port, "to startup!", flush=True)

            exit_code = check_server_process(process)

            if exit_code is None:
                print("Waiting for 10 seconds to see if the process is still running...", flush=True)
                time.sleep(10)
                check_server_process(process)

            sys.stdout.flush()
            sys.stderr.flush()
            sys.exit(1)
        count += 1

    print(f"Memgraph started in {count*0.01} seconds", flush=True)
    time.sleep(delay)


def generate_result_csv(suite, result_path):
    if not os.path.exists(result_path):
        return ""
    with open(result_path) as f:
        result = json.load(f)
    ret = ""
    for i in ["total", "passed"]:
        ret += "{},{},{}\n".format(suite, i, result[i])
    return ret


def generate_status(suite, result_path, required):
    if not os.path.exists(result_path):
        return ("Internal error!", 0, 1)
    with open(result_path) as f:
        result = json.load(f)
    total = result["total"]
    passed = result["passed"]
    ratio = passed / total
    msg = "{} / {} ({:.2%})".format(passed, total, ratio)
    if required:
        if passed == total:
            msg += " &#x1F44D;"  # +1 emoji
        else:
            msg += " &#x26D4;"  # no entry emoji
    return (msg, passed, total)


def generate_result_html(data):
    ret = "<!DOCTYPE html>\n"
    ret += "<h1>Memgraph GQL Behave Tests Status</h1>\n"
    ret += "<table>\n"
    for row in data:
        ret += "  <tr>\n"
        for item in row:
            if row == data[0]:
                fmt = "    <th>{}</th>\n"
            else:
                fmt = "    <td>{}</td>\n"
            ret += fmt.format(item)
        ret += "  </tr>\n"
    ret += "</table>\n"
    return ret


class MemgraphRunner:
    def __init__(self, build_directory):
        self.build_directory = build_directory
        self.proc_mg = None
        self.args = []

    def start(self, args=[]):
        if args == self.args and self.is_running():
            return

        self.stop()
        self.args = copy.deepcopy(args)

        self.data_directory = tempfile.TemporaryDirectory()
        memgraph_binary = os.path.join(self.build_directory, "memgraph")
        log_dir = os.path.join(BASE_DIR, "tests", "gql_behave", "memgraph-logs")
        os.makedirs(log_dir, exist_ok=True)
        args_mg = [
            memgraph_binary,
            "--storage-properties-on-edges=true",
            "--data-directory",
            self.data_directory.name,
            "--log-file",
            str(os.path.join(log_dir, "memgraph.log")),
            "--log-level",
            LOG_LEVEL,
            "--query-modules-directory",
            str(os.path.join(BUILD_DIR, "query_modules")),
        ]
        self.proc_mg = subprocess.Popen(args_mg + self.args)
        wait_for_server(7687, self.proc_mg, 1)
        assert self.is_running(), "The Memgraph process died!"

    def is_running(self):
        if self.proc_mg is None:
            return False
        if self.proc_mg.poll() is not None:
            return False
        return True

    def stop(self):
        if not self.is_running():
            return
        self.proc_mg.terminate()
        code = self.proc_mg.wait()
        assert code == 0, "The Memgraph process exited with non-zero!"


def main():
    # Parse args
    argp = argparse.ArgumentParser()
    argp.add_argument("--build-directory", default=BUILD_DIR)
    argp.add_argument("--cluster-size", default=3, type=int)
    args = argp.parse_args()

    # Load tests from config file
    with open(os.path.join(TESTS_DIR, "config.yaml")) as f:
        suites = yaml.safe_load(f)

    # venv used to run the qa engine
    venv_python = os.path.join(BASE_DIR, "tests", "ve3", "bin", "python3")

    # Temporary directory for suite results
    output_dir = tempfile.TemporaryDirectory()

    # Memgraph runner
    memgraph = MemgraphRunner(args.build_directory)

    @atexit.register
    def cleanup():
        memgraph.stop()

    # Results storage
    result_csv = "suite,status,quantity\n"
    status_data = [["Suite", "Status"]]
    mandatory_fails = []

    # Run suites
    old_storage_mode = ""
    for suite in suites:
        print("Starting suite '{}' scenarios.".format(suite["name"]))

        suite_storage_mode = suite["storage_mode"]
        if old_storage_mode != suite_storage_mode:
            memgraph.stop()
            memgraph.start(["--storage-mode", suite_storage_mode])
            old_storage_mode = suite_storage_mode

        suite["stats_file"] = os.path.join(output_dir.name, suite["name"] + ".json")
        cmd = [
            venv_python,
            "-u",
            os.path.join(SCRIPT_DIR, "run.py"),
            "--stats-file",
            suite["stats_file"],
            suite["test_suite"],
            "--storage-mode",
            suite_storage_mode,
        ]

        # The exit code isn't checked here because the `behave` framework
        # returns a non-zero exit code when some tests fail.
        subprocess.run(cmd)

        suite_status, suite_passed, suite_total = generate_status(
            suite["name"], suite["stats_file"], suite["must_pass"]
        )

        status_data.append([suite["name"], suite_status])
        result_csv += generate_result_csv(suite["name"], suite["stats_file"])

        if suite["must_pass"] and suite_passed != suite_total:
            mandatory_fails.append(suite["name"])
            break

    # Create status message
    result_html = generate_result_html(status_data)

    # Create the report file
    result_html_path = os.path.join(SCRIPT_DIR, "gql_behave_status.html")
    with open(result_html_path, "w") as f:
        f.write(result_html)

    # Create the measurements file
    result_csv_path = os.path.join(SCRIPT_DIR, "gql_behave_status.csv")
    with open(result_csv_path, "w") as f:
        f.write(result_csv)

    print(f"CSV status is generated in {result_csv_path}")
    print(f"HTML status is generated in {result_html_path}")

    # Check if tests failed
    if mandatory_fails != []:
        sys.exit(f"Some tests that must pass have failed: {mandatory_fails}")


if __name__ == "__main__":
    main()
